#!/usr/bin/env python


#	Copyright (C) 2008  Marco Reichwald <code@marconfus.org>

#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#	   http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

import os, sys, md5, re, string, time
import fuse
from errno import *
from stat import *
from fuse import Fuse

if not hasattr(fuse, '__version__'):
	raise RuntimeError, \
		"your fuse-py doesn't know of fuse.__version__, probably it's too old."

if sys.platform=="darwin":
	import unicodedata

fuse.fuse_python_api = (0, 2)
fuse.feature_assert('stateful_files', 'has_init')

global recordings, fileext
fileext=".mpg"
recordings=[]

def debug(str,level="normal"):
	if level != "error":
		return
	print str
	
class MyStat(fuse.Stat):
	def __init__(self):
		self.st_mode = 0
		self.st_ino = 0
		self.st_dev = 0
		self.st_nlink = 0
		self.st_uid = 0
		self.st_gid = 0
		self.st_size = 0
		self.st_atime = 0
		self.st_mtime = 0
		self.st_ctime = 0
		

class VDRfs(Fuse):

	class Recording(object):
		
		def __init__(self, thisdir):
	
			vfilename=""
			dir=""
			files={}
			filesize=0
	
			m=md5.new()
			for line in open(thisdir+"/"+"info.vdr"):
				# C	<channel id> <channel name>
				# E	<event id> <start time> <duration> <table id> <version>
				# T	<title>
				# S	<short text>
				# D	<description>
				# X	<stream> <type> <language> <descr>
				# V	<vps time>
				m.update(line)
				line=unicode(line, "iso-8859-1").encode("utf-8")
				#if sys.platform=="darwin":
				#	line=unicode(line, "utf-8")
				#	line=unicodedata.normalize("NFD", line)

				if line[0] == "T":
					self.title=line[2:-1]
				elif line[0] == "S":
					self.shorttext=line[2:-1]
				elif line[0] == "D":
					self.description=line[2:-1]
				elif line[0] == "C":
					self.channel=line[2:-1]
				elif line[0] == "E":
					try:
						self.starttime=string.atoi(line.split()[2])
					except:
						pass
	
			self.md5=m.hexdigest()
			self.dir=thisdir
		
			self.files={}
			self.filesize=0
			for f in os.listdir(thisdir):
				if re.match("\d\d\d\.vdr", f):
					thisfilesize=os.stat(thisdir+"/"+f)[6]
					self.files[f]=thisfilesize
					self.filesize+=thisfilesize
			
			self.vfilename=self.title+fileext
			self.multipleepisodes=False
			self.notlastepisode=False
			self.categories=[]

	def __init__(self, *args, **kw):
		Fuse.__init__(self, *args, **kw)
		self.recordingsdir=""
		self.fileext=""
		self.enablecategories=False
		self.catonlysinglewords=True
		self.categoriesfrompath=False


	def listbaseobjects(self, cat=""):
		d=[]
		for e in recordings:
			if e.multipleepisodes and e.notlastepisode:
				pass
			else:
				if (cat=="") or (cat in e.categories) or (cat=="No Categories" and e.categories==[]):
					d.append(e.vfilename)
		return(d)

	def listepisodes(selfself, title):
		d=[]
		for e in recordings:
			if e.title==title:
				d.append(e.episode+fileext)
		return d
	
	def builddir(self,path):
	   d=[]
	   if (path=="/" and self.enablecategories):
		   return (["All","Categories"]) 
	   
	   if (path=="/All" and self.enablecategories) or (path=="/" and not self.enablecategories):
		   return (self.listbaseobjects())
					   
	   if (path=="/Categories" and self.enablecategories):
		   d.append("No Categories")
		   for c in self.allcategories:
			   d.append(c)
		   return d
	   
	   if (path.startswith("/All/") and self.enablecategories):
		   title=path.split("/")[2]
		   return (self.listepisodes(title))
		   
	   if (path.startswith("/Categories/") and self.enablecategories):
		   cat=path.split("/")[2]
		   if path.count("/")==2:
			   return (self.listbaseobjects(cat))
		   else:
			   title=path.split("/")[3]
			   return (self.listepisodes(title))
   
	def readdir(self, path, offset):
		# no error is thrown if path does not exist
		
		for d in self.builddir(path):
			yield(fuse.Direntry(d))


	def getrecordingfrompath(self,path):
		parts=path.split("/")
		
		for r in recordings:
			if (parts[-1]==r.vfilename) or (parts[-1]==r.episode+fileext and parts[-2]==r.title):
				return(r)
			
		return None
		
	def getattr(self, path):
		if path == '/':
			return(self.rootdirstat)
		if self.enablecategories and (path=="/All" or path=="/Categories" or (path.startswith("/Categories/") and path.count("/")==2)):
			return(self.rootdirstat)

		st=MyStat()
		r=self.getrecordingfrompath(path)
		if r is None:
			return -1
		if r.starttime:
			st.st_atime=r.starttime
			st.st_mtime=r.starttime
			st.st_ctime=r.starttime
		if path.endswith(fileext):
			st.st_size=r.filesize
			st.st_mode = S_IFREG | 0444
		else:
			st.st_mode = S_IFDIR | 0755
			size=0
		return st
						

	def fsinit(self):
		return
  
	class RecordingFile(object):

		def getrecordingfrompath(self,path):
			parts=path.split("/")
			
			if parts[-1].endswith(fileext):
				for r in recordings:
					if (parts[-1]==r.vfilename) or (parts[-1]==r.episode+fileext and parts[-2]==r.title):
						return(r)
			
			return None

		def __init__(self, path, flags, *mode):
		
			self.vdrdir=""
			self.vdrfilesize={}
			self.vdrfilenames=[]
			self.files={}
			self.fds={}
			
			r=self.getrecordingfrompath(path)
				
			if not r:
				return None

			self.vdrfilesize=r.files
			self.vdrfilenames=r.files.keys()
			self.vdrfilenames.sort()
			self.vdrdir=r.dir

			for f in self.vdrfilenames:
				thisfile=os.fdopen(os.open(self.vdrdir+"/"+f, os.O_RDONLY), "r")
				self.files[f]=thisfile
				self.fds[f]=thisfile.fileno()
				
		def read(self, length, offset):
			
			toff=0
			tsz=0
			data=""
			origlength=length
			thisfile=None
			for f in self.vdrfilenames:
				tsz=toff+self.vdrfilesize[f]
				if  offset<= tsz:
					thisfile=self.files[f]
					thisfile.seek(offset-toff)
					if (offset-toff+length)>self.vdrfilesize[f]:
						partialread=self.vdrfilesize[f]-(offset-toff)
						data+=thisfile.read(partialread)
						
						offset+=partialread
						length-=partialread
					else:
						data+=thisfile.read(length)
						break
				toff+=self.vdrfilesize[f]
			
			if thisfile==None:
				return -1
			else:	
				return data

		def release(self, flags):
			for f in self.files.keys():
				self.files[f].close()

	def dogetcategoriesfrompath(self, r):
		path=r.dir
		
		cats=[]
		for d in self.recordingdirs:
			if path.find(d+"/")==0:
				path=path[len(d)+1:]
				if path.count("/")>1:
					cats=path.split("/")[:-2]
				break
		
		return cats
		
	def readrecordings(self, dir):
		for root, dirs, files in os.walk(dir):
			if "info.vdr" in files:
				tr=self.Recording(root)
				for i in recordings:
					if i.title == tr.title:
						tr.multipleepisodes=True
						i.multipleepisodes=True
						i.notlastepisode=True
						if i.vfilename[-len(fileext):] == fileext:
							i.vfilename=i.vfilename[:-len(fileext)]
						
				tr.episode=time.strftime("%d-%m-%y", time.gmtime(tr.starttime))
				if tr.multipleepisodes:
					tr.vfilename=tr.vfilename[:-len(fileext)]
					i=len([r for r in recordings if (r.episode==tr.episode and r.title==tr.title)])
					if i>0:
						tr.episode+="-"+str(i)
				if self.enablecategories and self.categoriesfrompath:
					tr.categories=self.dogetcategoriesfrompath(tr)
					
				recordings.append(tr)
		
		if self.categoriesfrompath:
			for r in recordings:
				for c in r.categories:
					if c not in self.allcategories:
						if self.catonlysinglewords and (c.find("_")<0) and (c.find(" ")<0):
							self.allcategories.append(c)
						elif not self.catonlysinglewords:
							self.allcategories.append(c)
				
	def main(self, *a, **kw):

		self.file_class = self.RecordingFile
		
		if self.recordingsdir.find(":"):
			self.recordingdirs=self.recordingsdir.split(":")
		else:
			self.recordingdirs=[self.recordingsdir]
		
		self.allcategories=[]	
		for dir in self.recordingdirs:
			self.readrecordings(dir)
			rst=os.lstat(dir)
			self.rootdirstat=rst
		
		return Fuse.main(self, *a, **kw)


def main():

	global fileext
	
	usage = """
Userspace filesystem for VDR recordings

""" + Fuse.fusage

	server = VDRfs(version="%prog " + fuse.__version__,
				 usage=usage,
				 dash_s_do='setsingle')

	server.parser.add_option(mountopt="recordingsdir", metavar="PATH", default='/media/vdrrecordings',
							 help="path to vdr recordings [default: %default]")
	server.parser.add_option(mountopt="fileext", metavar="EXT", default='.mpg',
							 help="extension to append to filename [default: %default]")
	server.parser.add_option(mountopt="enablecategories", default=False, help="enable categories as subdirs [default: %default]",
							 action="store_true")
	server.parser.add_option(mountopt="categoriesfrompath", default=False, help="convert subdirs to categories [default: %default]",
							 action="store_true")
	server.parser.add_option(mountopt="catonlysinglewords", default=True, help="if above enabled, only use single words [default: %default]",
							 action="store_true")
	
	sys.argv.append("-o")
	sys.argv.append("fsname=VDR,fstypename=vdrfs")
	
	if sys.platform=="darwin":
		sys.argv.append("-o")
		sys.argv.append("volname=VDR")
		
	server.parse(values=server, errex=1)

	print "enable categories: "+str(server.enablecategories)
	if server.fileext:
		fileext=server.fileext
		
	server.main()


if __name__ == '__main__':
	main()
